package cc.siyecao.mapper.interceptor;

import cc.siyecao.mapper.Criteria;
import cc.siyecao.mapper.Criterion;
import cc.siyecao.mapper.common.core.Code;
import cc.siyecao.mapper.common.exception.AssertException;
import cc.siyecao.mapper.common.util.Assert;
import cc.siyecao.mapper.common.util.StringUtils;
import cc.siyecao.mapper.enums.SqlKeyword;
import cc.siyecao.mapper.provider.annotation.FieldFill;
import cc.siyecao.mapper.provider.annotation.Key;
import cc.siyecao.mapper.provider.annotation.Version;
import cc.siyecao.mapper.provider.entity.*;
import cc.siyecao.mapper.provider.enums.FillStrategy;
import cc.siyecao.mapper.provider.enums.KeyType;
import cc.siyecao.mapper.provider.handlers.MetaObjectHandler;
import cc.siyecao.mapper.provider.incrementer.IdentifierGenerator;
import cc.siyecao.mapper.provider.util.ServiceLoaderUtil;
import cc.siyecao.mapper.wrapper.IWrapper;
import cc.siyecao.mapper.wrapper.Wrapper;
import org.apache.ibatis.builder.annotation.ProviderContext;
import org.apache.ibatis.builder.annotation.ProviderSqlSource;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.SimpleTypeRegistry;
import org.springframework.util.ReflectionUtils;

import javax.persistence.Id;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class FieldFillInterceptor implements Interceptor {
    private static final Log log = LogFactory.getLog( FieldFillInterceptor.class );
    private MappedStatement mappedStatement;
    private Object parameterObject;
    private Configuration configuration;
    private SqlCommandType sqlCommandType;
    /**
     * 缓存方法对应的实体
     */
    private static final Map<String, EntityTable> CACHE_ENTITY = new ConcurrentHashMap<>( 16 );


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        this.mappedStatement = (MappedStatement) args[0];
        this.parameterObject = args[1];
        this.configuration = this.mappedStatement.getConfiguration();
        this.sqlCommandType = this.mappedStatement.getSqlCommandType();

        /* 只处理插入或更新操作 */
        if (this.parameterObject != null && (SqlCommandType.INSERT == this.sqlCommandType || SqlCommandType.UPDATE == this.sqlCommandType)) {
            //检查 parameterObject
            if (!SimpleTypeRegistry.isSimpleType( this.parameterObject.getClass() )) {
                Map<String, Object> insertFillMap = new HashMap<>();
                Map<String, Object> updateFillMap = new HashMap<>();
                List<MetaObjectHandler> metaObjectHandlers = ServiceLoaderUtil.getInstances( MetaObjectHandler.class );
                if (metaObjectHandlers != null && metaObjectHandlers.size() > 0) {
                    Assert.isTrue( metaObjectHandlers.size() == 1, Code.FIND_MULT_IMPL );
                    MetaObjectHandler metaObjectHandler = metaObjectHandlers.get( 0 );
                    insertFillMap = metaObjectHandler.insertFill();
                    updateFillMap = metaObjectHandler.updateFill();
                }
                fillField( this.parameterObject, insertFillMap, updateFillMap );
            }
        }
        return invocation.proceed();
    }

    private void fillField(Object parameter, Map<String, Object> insertFillMap, Map<String, Object> updateFillMap) {
        if (parameter != null) {
            EntityTable entityTable = null;
            Object entity = parameter;
            if (parameter instanceof Map) {
                Collection<Object> parameters = getParameters( parameter );
                parameters.forEach( parameter1 -> fillField( parameter1, insertFillMap, updateFillMap ) );
            } else if (parameter instanceof List) {
                List parameters = (List) parameter;
                parameters.forEach( parameter1 -> fillField( parameter1, insertFillMap, updateFillMap ) );
            } else if (parameter instanceof IWrapper) {
                if (SqlCommandType.UPDATE == this.sqlCommandType) {
                    Wrapper wrapper = (Wrapper) parameter;
                    fillWrapper( wrapper, updateFillMap );
                }
            } else if (parameter instanceof Table) {
                Table table = (Table) parameter;
                fillDynamicTable( table, insertFillMap, updateFillMap );
            } else {
                entityTable = EntityFactory.create( entity.getClass() );
            }
            if (entityTable != null) {
                //到这里就应该转换到实体参数对象了,因为填充和ID处理都是针对实体对象处理的,不用传递原参数对象下去.
                MetaObject metaObject = this.configuration.newMetaObject( entity );
                List<EntityColumn> entityColumns = entityTable.columns();
                for (Iterator<EntityColumn> iterator = entityColumns.iterator(); iterator.hasNext(); ) {
                    EntityColumn entityColumn = iterator.next();
                    EntityField entityField = entityColumn.field();
                    if (SqlCommandType.INSERT == this.sqlCommandType) {
                        if (entityField.isAnnotationPresent( Id.class ) && entityField.isAnnotationPresent( Key.class )) {
                            populateKeys( entityColumn, entityField, metaObject );
                        }
                        if (entityField.isAnnotationPresent( Version.class )) {
                            populateVersion( entityField, metaObject );
                        }
                    }
                    if (entityField.isAnnotationPresent( FieldFill.class )) {
                        FieldFill fieldFill = entityField.getAnnotation( FieldFill.class );
                        String fieldName = entityField.getName();
                        if (SqlCommandType.INSERT == this.sqlCommandType) {
                            if (fieldFill.fill() == FillStrategy.INSERT || fieldFill.fill() == FillStrategy.INSERT_UPDATE) {
                                if (fieldFill.force()) {
                                    insertFill( metaObject, fieldName, insertFillMap );
                                } else if (StringUtils.checkValNull( getFieldValByName( fieldName, metaObject ) )) {
                                    insertFill( metaObject, fieldName, insertFillMap );
                                }
                            }
                        } else if (SqlCommandType.UPDATE == this.sqlCommandType) {
                            if (fieldFill.fill() == FillStrategy.UPDATE || fieldFill.fill() == FillStrategy.INSERT_UPDATE) {
                                if (fieldFill.force()) {
                                    updateFill( metaObject, fieldName, updateFillMap );
                                } else if (StringUtils.checkValNull( getFieldValByName( fieldName, metaObject ) )) {
                                    updateFill( metaObject, fieldName, updateFillMap );
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    private void fillDynamicTable(Table table, Map<String, Object> insertFillMap, Map<String, Object> updateFillMap) {
        EntityTable entityTable = EntityFactory.create( table.getClass() );
        entityTable.table( table.table() );//赋值正确的表名
        List<EntityColumn> entityColumns = entityTable.columns();
        MetaObject metaObject = this.configuration.newMetaObject( table );
        Map<String, Object> dynamicParams = table.dynamicParams();
        for (Iterator<EntityColumn> iterator = entityColumns.iterator(); iterator.hasNext(); ) {
            EntityColumn entityColumn = iterator.next();
            EntityField entityField = entityColumn.field();
            if (SqlCommandType.INSERT == this.sqlCommandType) {
                if (entityField.isAnnotationPresent( Id.class ) && entityField.isAnnotationPresent( Key.class )) {
                    populateKeys( entityColumn, entityField, dynamicParams );
                }
            }
            String fieldName = entityField.getName();
            if (entityField.isAnnotationPresent( FieldFill.class ) && updateFillMap.containsKey( fieldName )) {
                FieldFill fieldFill = entityField.getAnnotation( FieldFill.class );
                if (SqlCommandType.UPDATE == this.sqlCommandType) {
                    if (fieldFill.fill() == FillStrategy.UPDATE || fieldFill.fill() == FillStrategy.INSERT_UPDATE) {
                        if (fieldFill.force() || dynamicParams.get( fieldName ) == null) {
                            dynamicParams.put( fieldName, updateFillMap.get( fieldName ) );
                        }
                    }
                } else if (SqlCommandType.INSERT == this.sqlCommandType) {
                    if (fieldFill.fill() == FillStrategy.INSERT || fieldFill.fill() == FillStrategy.INSERT_UPDATE) {
                        dynamicParams.put( fieldName, insertFillMap.get( fieldName ) );
                    }
                }
            }
        }
        metaObject.setValue( "dynamicParams", dynamicParams );
    }

    private void fillWrapper(Wrapper wrapper, Map<String, Object> updateFillMap) {
        if (wrapper.getSetCriteria().isValid()) {
            EntityTable entityTable = getEntity();
            Criteria setCriteria = wrapper.getSetCriteria();
            List<Criterion> criterionList = setCriteria.getCriterionList();
            Map<String, Criterion> criterionMap = setCriteria.getCriterionMap();
            List<EntityColumn> entityColumns = entityTable.columns();
            for (Iterator<EntityColumn> iterator = entityColumns.iterator(); iterator.hasNext(); ) {
                EntityColumn entityColumn = iterator.next();
                EntityField entityField = entityColumn.field();
                if (entityField.isAnnotationPresent( FieldFill.class )) {
                    FieldFill fieldFill = entityField.getAnnotation( FieldFill.class );
                    String fieldName = entityField.getName();
                    String columnName = entityColumn.column();
                    if (updateFillMap.containsKey( fieldName ) && (fieldFill.fill() == FillStrategy.UPDATE || fieldFill.fill() == FillStrategy.INSERT_UPDATE)) {
                        Criterion criterion = criterionMap.get( columnName );
                        if (criterion != null) {
                            if (fieldFill.force()) {
                                criterionList.remove( criterion );
                                criterion.setValue( updateFillMap.get( fieldName ) );
                                criterionList.add( criterion );
                            } else if (StringUtils.checkValNull( criterion.getValue() )) {
                                criterion.setValue( updateFillMap.get( fieldName ) );
                                criterionList.add( criterion );
                            }
                        } else {
                            setCriteria.addCriterion( columnName, SqlKeyword.EQ, updateFillMap.get( fieldName ) );
                        }
                    }
                }
            }
        }
    }

    private EntityTable getEntity() {
        String cacheKey = this.mappedStatement.getId();
        if (!CACHE_ENTITY.containsKey( cacheKey )) {
            synchronized (cacheKey) {
                try {
                    ProviderSqlSource providerSqlSource = (ProviderSqlSource) this.mappedStatement.getSqlSource();
                    Field field = ReflectionUtils.findField( ProviderSqlSource.class, "providerContext", ProviderContext.class );
                    field.setAccessible( true );
                    ProviderContext providerContext = (ProviderContext) field.get( providerSqlSource );
                    EntityTable entityTable = EntityFactory.create( providerContext.getMapperType(), providerContext.getMapperMethod() );
                    CACHE_ENTITY.put( cacheKey, entityTable );
                } catch (Exception exception) {
                    exception.printStackTrace();
                }
            }
        }
        return CACHE_ENTITY.get( cacheKey );
    }

    /**
     * 处理正常批量插入逻辑
     * <p>
     * org.apache.ibatis.session.defaults.DefaultSqlSession$StrictMap 该类方法
     * wrapCollection 实现 StrictMap 封装逻辑
     * </p>
     *
     * @return 集合参数
     */
    @SuppressWarnings({"rawtypes", "unchecked"})
    protected Collection<Object> getParameters(Object parameterObject) {
        Collection<Object> parameters = new ArrayList<>();
        Map parameterMap = (Map) parameterObject;
        if (parameterMap.containsKey( "entityList" )) {
            Collection collection = (Collection) parameterMap.get( "entityList" );
            parameters.addAll( collection );
        }
        if (parameterMap.containsKey( "entity" )) {
            parameters.add( parameterMap.get( "entity" ) );
        }
        if (parameterMap.containsKey( "wrapper" )) {
            parameters.add( parameterMap.get( "wrapper" ) );
        }
        if (parameterMap.containsKey( "dynamicTable" )) {
            parameters.add( parameterMap.get( "dynamicTable" ) );
        }
        return parameters;
    }

    protected void populateVersion(EntityField entityField, MetaObject metaObject) {
        String keyProperty = entityField.getName();
        metaObject.setValue( keyProperty, 1 );
    }

    protected void populateKeys(EntityColumn entityColumn, EntityField entityField, MetaObject metaObject) {
        String keyProperty = entityField.getName();
        Object idValue = metaObject.getValue( keyProperty );
        if (StringUtils.checkValNull( idValue )) {
            Key key = entityField.getAnnotation( Key.class );
            Class idClass = key.generator();
            String seqName = key.seqName();
            KeyType keyType = key.type();
            if (keyType == KeyType.GENERATOR && idClass != null && idClass != Class.class) {
                final IdentifierGenerator identifierGenerator;
                try {
                    identifierGenerator = (IdentifierGenerator) idClass.newInstance();
                } catch (Exception e) {
                    throw new AssertException( "IdentifierGenerator type '" + idClass.getName() + "' not supported" );
                }
                Object objId = identifierGenerator.nextId();
                Class<?> fieldType = entityField.getType();
                if (Number.class.isAssignableFrom( fieldType )) {
                    try {
                        Number id = (Number) objId;
                        if (fieldType == id.getClass()) {
                            metaObject.setValue( keyProperty, id );
                        } else if (Integer.class == fieldType) {
                            metaObject.setValue( keyProperty, id.intValue() );
                        } else if (Long.class == fieldType) {
                            metaObject.setValue( keyProperty, id.longValue() );
                        } else if (BigDecimal.class.isAssignableFrom( fieldType )) {
                            metaObject.setValue( keyProperty, new BigDecimal( id.longValue() ) );
                        } else if (BigInteger.class.isAssignableFrom( fieldType )) {
                            metaObject.setValue( keyProperty, new BigInteger( id.toString() ) );
                        } else {
                            throw new AssertException( "Key type '" + keyType + "' not supported" );
                        }
                    } catch (ClassCastException e) {
                        throw new AssertException( idClass.getName() + ".nextId() 返回类型与主键类型不匹配" );
                    }
                } else {
                    metaObject.setValue( keyProperty, identifierGenerator.nextId().toString() );
                }
            } else if (keyType == KeyType.SEQUENCE && StringUtils.isNotEmpty( seqName )) {
                Connection connection = null;
                PreparedStatement stmt = null;
                ResultSet rs = null;
                try {
                    connection = this.configuration.getEnvironment().getDataSource().getConnection();
                    String sql = "SELECT " + seqName + ".NEXTVAL AS ID FROM DUAL";
                    stmt = connection.prepareStatement( sql );
                    rs = stmt.executeQuery();
                    rs.next();
                    Long id = rs.getLong( 1 );
                    metaObject.setValue( keyProperty, id );
                } catch (SQLException e) {
                    e.printStackTrace();
                } finally {
                    if (connection != null) {
                        try {
                            connection.close();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    }
                    if (stmt != null) {
                        try {
                            stmt.close();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    }
                    if (rs != null) {
                        try {
                            rs.close();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    }
                }
            } else {
                Connection connection = null;
                PreparedStatement stmt = null;
                ResultSet rs = null;
                try {
                    connection = this.configuration.getEnvironment().getDataSource().getConnection();
                    stmt = connection.prepareStatement( "SELECT max(" + entityColumn.column() + ") FROM " + entityColumn.entityTable().table() );
                    rs = stmt.executeQuery();
                    rs.next();
                    Long id = rs.getLong( 1 );
                    System.out.println( "id = " + id );
                    metaObject.setValue( keyProperty, id + 1 );
                } catch (SQLException e) {
                    e.printStackTrace();
                } finally {
                    if (connection != null) {
                        try {
                            connection.close();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    }
                    if (stmt != null) {
                        try {
                            stmt.close();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    }
                    if (rs != null) {
                        try {
                            rs.close();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }

    protected void populateKeys(EntityColumn entityColumn, EntityField entityField, Map<String, Object> dynamicParams) {
        String keyProperty = entityField.getName();
        Object idValue = dynamicParams.get( keyProperty );
        if (StringUtils.checkValNull( idValue )) {
            Key key = entityField.getAnnotation( Key.class );
            Class idClass = key.generator();
            String seqName = key.seqName();
            KeyType keyType = key.type();
            if (keyType == KeyType.GENERATOR && idClass != null && idClass != Class.class) {
                final IdentifierGenerator identifierGenerator;
                try {
                    identifierGenerator = (IdentifierGenerator) idClass.newInstance();
                } catch (Exception e) {
                    throw new AssertException( "IdentifierGenerator type '" + idClass.getName() + "' not supported" );
                }
                Object objId = identifierGenerator.nextId();
                dynamicParams.put( keyProperty, objId );
            } else if (keyType == KeyType.SEQUENCE && StringUtils.isNotEmpty( seqName )) {
                Connection connection = null;
                PreparedStatement stmt = null;
                ResultSet rs = null;
                try {
                    connection = this.configuration.getEnvironment().getDataSource().getConnection();
                    String sql = "SELECT " + seqName + ".NEXTVAL AS ID FROM DUAL";
                    stmt = connection.prepareStatement( sql );
                    rs = stmt.executeQuery();
                    rs.next();
                    Long id = rs.getLong( 1 );
                    dynamicParams.put( keyProperty, id );
                } catch (SQLException e) {
                    e.printStackTrace();
                } finally {
                    if (connection != null) {
                        try {
                            connection.close();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    }
                    if (stmt != null) {
                        try {
                            stmt.close();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    }
                    if (rs != null) {
                        try {
                            rs.close();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    }
                }
            } else {
                Connection connection = null;
                PreparedStatement stmt = null;
                ResultSet rs = null;
                try {
                    connection = this.configuration.getEnvironment().getDataSource().getConnection();
                    stmt = connection.prepareStatement( "SELECT max(" + entityColumn.column() + ") FROM " + entityColumn.entityTable().table() );
                    rs = stmt.executeQuery();
                    rs.next();
                    Long id = rs.getLong( 1 );
                    System.out.println( "id = " + id );
                    dynamicParams.put( keyProperty, id + 1 );
                } catch (SQLException e) {
                    e.printStackTrace();
                } finally {
                    if (connection != null) {
                        try {
                            connection.close();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    }
                    if (stmt != null) {
                        try {
                            stmt.close();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    }
                    if (rs != null) {
                        try {
                            rs.close();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }

    protected void insertFill(MetaObject metaObject, String fieldName, Map<String, Object> insertFillMap) {
        Object fieldVal = insertFillMap.get( fieldName );
        if (Objects.nonNull( fieldVal ) && metaObject.hasSetter( fieldName )) {
            metaObject.setValue( fieldName, fieldVal );
        }
    }

    protected void updateFill(MetaObject metaObject, String fieldName, Map<String, Object> updateFillMap) {
        Object fieldVal = updateFillMap.get( fieldName );
        if (Objects.nonNull( fieldVal ) && metaObject.hasSetter( fieldName )) {
            metaObject.setValue( fieldName, fieldVal );
        }
    }

    protected Object getFieldValByName(String fieldName, MetaObject metaObject) {
        return metaObject.hasGetter( fieldName ) ? metaObject.getValue( fieldName ) : null;
    }
}
